/* @flow */

import { emptyNode } from 'core/vdom/patch'
import { resolveAsset, handleError } from 'core/util/index'
import { mergeVNodeHook } from 'core/vdom/helpers/index'

//该模块用于处理标签上未处理的指令，也就是vnode.data.directives数组
export default {
  //对于create钩子来说，updateDirectives方法的第一个参数为emptyNode对象
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

/**
 * 示例：
 * template模板字符串为：
 * <div>
 *     <div id="one" v-if="t === 1" v-test1>{{t1}}</div>
 *     <div id="two" v-else v-test2>{{t2}}</div>
 * </div>
 * 其中存在两个指令test1和test2，在1和2之间来回切换t的值时，div#one和div#two的
 * vnode对象会分别作为_update方法的前一个或者后一个参数，比如oldVnode为
 * div#one，vnode为div#two，则就会出现好像指令被删除的效果，其实只是两个vnode之间的
 * 替换，单个vnode对象上的指令不会被新增或者删除
 */
function _update (oldVnode, vnode) { //如上例，假设oldVnode为div#one，vnode为div#two
  const isCreate = oldVnode === emptyNode //如上例，false
  const isDestroy = vnode === emptyNode //如上例，false
  //对象形式的directives
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) //如上例，指令为v-test1
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) //如上例，指令为v-test2

  const dirsWithInsert = []
  const dirsWithPostpatch = []

  let key, oldDir, dir
  for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
    if (!oldDir) { //如上例，v-test2在oldDir中不存在
      // new directive, bind
      // bind和inserted都只会被调用一次，并且只在初次创建vue实例时调用，
      // 之后的更新会调用update和componentUpdated
      callHook(dir, 'bind', vnode, oldVnode) //如上例，调用v-test2指令
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
      }
    } else { //如上例，如果更新t2的值，则会运行到此处
      // existing directive, update
      // 在指令对象上设置oldValue，用于保存之前的值，在调用钩子的回调函数时，可以供用户使用
      dir.oldValue = oldDir.value
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir)
      }
    }
  }

  if (dirsWithInsert.length) {
    //被绑定元素插入父节点时调用，仅调用一次
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    if (isCreate) {
      //直接对vnode.data.hook对象进行修改，向其中加入insert的hook
      //mergeVNodeHook方法用于创建一个函数A，该函数A会调用callInsert方法，并在调用之后从hook中删除该
      //函数A，保证callInsert方法只调用一次，防止callInsert方法被多次调用
      //此时是create，是初次创建vue实例，相应的el dom元素还没有创建。
      mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert)
    } else { //如上例，isCreate为false，运行到此处
      //运行到这里，一定是调用了update钩子方法，既不可能是create，因为此时isCreate为false，
      //也不可能是destroy，因为如果是destroy，则vnode为emptyVnode，则dirsWithInsert数组不可能有数据。
      //此时是update，所以el dom元素一定已经创建完成，因为根本就没有重新创建，而只是简单的将oldVnode的el赋值到
      //vnode的el，所以这里直接运行callInsert方法。
      callInsert()
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }

  //如果isCreate为true，则oldVnode为emptyVnode，
  //则oldDirs为空对象，所以无需执行if中的语句块
  if (!isCreate) { //如上例，isCreate为false
    for (key in oldDirs) { //如上例，oldDirs为v-test1
      if (!newDirs[key]) {
        // no longer present, unbind
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy) //如上例，v-test1指令的uhbind方法被调用
      }
    }
  }
}

const emptyModifiers = Object.create(null)

//该方法用于将数组形式的dirs转换为对象，其中对象的键为
//通过getRawDirName方法转换而来（可查看源码），值为VNodeDirective对象，
//只不过在该值上新添加了一个def属性，用于保存对应注册的指令对象
function normalizeDirectives (
  dirs: ?Array<VNodeDirective>,
  vm: Component
): { [key: string]: VNodeDirective } {
  const res = Object.create(null)
  if (!dirs) {
    return res
  }
  let i, dir
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i]
    if (!dir.modifiers) {
      dir.modifiers = emptyModifiers
    }
    res[getRawDirName(dir)] = dir
    //获得dir.name指定的、通过Vue.directive注册或者内置在Vue构造函数的选项对象中的自定义指令或者Vue的内建指令
    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
  }
  return res
}

function getRawDirName (dir: VNodeDirective): string {
  //示例：
  //<div v-focus:hello></div>
  //则rawName为'v-focus:hello'
  return dir.rawName || `${dir.name}.${Object.keys(dir.modifiers || {}).join('.')}`
}

//调用定义的指令中的相应的钩子方法
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  const fn = dir.def && dir.def[hook]
  if (fn) {
    try {
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
    } catch (e) {
      handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
    }
  }
}
